
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.PriorityQueue;
import java.util.ResourceBundle;

import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;

//@author A0096724W
public class CalendarTableController {
    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private static GridPane monthTable;
    
    @FXML
    private static AnchorPane monthContainer;
    
    @FXML
    private static Label monthLabel;

    //Comparator to sort Tasks by start date first
    private static Comparator<Task> comparator = new Comparator<Task>() {
	@Override
	public int compare(Task arg0, Task arg1) {
	
	    return arg0.getTimeSpan().compareTo(arg1.getTimeSpan());
	}    
    };
    
    //Comparator to sort Tasks by longest event first
    private static Comparator<Task> differenceComparator = new Comparator<Task>() {
   	@Override
   	public int compare(Task arg0, Task arg1) {

   	    double diff0 = calculateTimeDifference(arg0);
   	    double diff1 = calculateTimeDifference(arg1);
   	 
   	    if (diff0 == diff1) {
   		return 0;
   	    } else if (diff0 > diff1) {
   		return -1;
   	    } else {
   		return 1;
   	    }
   	}    
       };
	
	
    
    private static Calendar thisMonth;
    private static boolean resize;
    private static final double HEADING_OFFSET = 71;
    private static final double DATE_OFFSET = 25;
    private static final double ROW_OFFSET = 100;
    private static final double INDEX_OFFSET = 15;
    private static final double COL_OFFSET = 100;
    private static final String DESCRIPTION_OFFSET = "  ";
    private final static String[] days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
    
    //Array representation of CalendarTable
    private static int[][][] calendarMonth = new int[6][7][5];
    
    //In case of too much events, store the number of excess here
    private static int[][] eventOverFlow = new int[6][7];
    private static int[][] deadlineOverFlow = new int[6][7];
    
    @FXML
    void initialize() {
        assert monthTable != null : "fx:id=\"month\" was not injected: check your FXML file 'CalendarTable.fxml'.";
        assert monthContainer != null : "fx:id=\"monthContainer\" was not injected: check your FXML file 'CalendarTable.fxml'.";
        assert monthLabel != null : "fx:id=\"monthLabel\" was not injected: check your FXML file 'CalendarTable.fxml'.";
        
        monthTable.getStyleClass().add("calendarTable");
        monthContainer.getStyleClass().add("calendarTable");
    }

    
    public static void populateCalendarTable(PriorityQueue<Task> events, 
	    PriorityQueue<Task> deadline, Calendar calen) throws Exception {
	thisMonth = calen;
	
	resetCalendarTable();
	populateLayOut();
	
	ArrayList<Task> eventList = initListOfTasksInThisMonth(events);
	ArrayList<Task> deadlineList = initListOfTasksInThisMonth(deadline);
	
	fillTableWithTasks(eventList, false);
	fillTableWithTasks(deadlineList, true);
	
	fillInAllOverFlow();
	monthTable.setGridLinesVisible(true);
    }


    private static void fillTableWithTasks(ArrayList<Task> eventList, boolean isDeadline) {
	if (!eventList.isEmpty()) {
	    for (int i = 0; i < eventList.size(); i++) {
		fitCellToTable(eventList.get(i), isDeadline);
	    }
	}
    }


    private static ArrayList<Task> initListOfTasksInThisMonth(
	    PriorityQueue<Task> events) {
	ArrayList<Task> eventList = duplicateToArrayList(events);
	eventList = keepTasksInThisMonth(eventList);
	Collections.sort(eventList, differenceComparator);
	return eventList;
    }


    private static void resetCalendarTable() {
	calendarMonth = new int[6][7][5];
	eventOverFlow = new int[6][7];
	deadlineOverFlow = new int[6][7];
    }
    
    private static void fitCellToTable(Task t, boolean isDeadline) {
	
	//Get start and end of task
	Calendar start = dateToCal(t.getTimeSpan().getStart());
	Calendar end = dateToCal(t.getTimeSpan().getEnd());
	
	//Deadline task
	if (isDeadline) {
	    setDeadlineTaskToLastWholeDay(start, end);
	}
	
	//Get the starting and end dates
	int startDate = start.get(Calendar.DAY_OF_MONTH);
	int endDate = end.get(Calendar.DAY_OF_MONTH);
	
	//Get the first day of this month
	thisMonth.set(Calendar.DAY_OF_MONTH, 1);
	int day = thisMonth.get(Calendar.DAY_OF_WEEK);
	
	//Get the coordinates of the start date
	int row = 0;
	int col = day - 1;
	for (int i = 1; i < startDate; i++) {
	    col++;
	    if (col > 6) {
		col = col%7;
		row++;
	    }
	}
	
	boolean hasEnoughSpace = true;
	
	//If no more space in that date, add to overflow
	if (numberofEntries(calendarMonth[row][col]) >= 4) {
	    hasEnoughSpace = false;
	}
	
	int indexToFit = lookForSpareIndex(row, col);

	int tempRow = row;
	int tempCol = col;
	
	//Try to fill all the spaces required by the event at indexToFit
	for (int i = 0; i <= endDate - startDate; i++) {
	    if (tempCol > 6) {
		tempCol = tempCol%7;
		tempRow++;
	    }
	    
	    //If already has existing entry, there is not enough space
	    if (calendarMonth[tempRow][tempCol][indexToFit] == 1) {
		hasEnoughSpace = false;
		break;
	    }
	    tempCol++;
	}

	tempRow = row;
	tempCol = col;
	
	addItemsToTable(t, isDeadline, startDate, endDate, hasEnoughSpace,
		indexToFit, tempRow, tempCol);
    }


    private static void addItemsToTable(Task t, boolean isDeadline,
	    int startDate, int endDate, boolean hasEnoughSpace, int indexToFit,
	    int tempRow, int tempCol) {
	//If not enough space, add overflow in all
	if (!hasEnoughSpace) {
	    for (int i = 0; i <= endDate - startDate; i++) {
		if (tempCol > 6) {
		    tempCol = tempCol%7;
		    tempRow++;
		}
		
		addToOverFlowList(isDeadline, tempRow, tempCol);
		
		tempCol++;
	    }
	    //Fill the spaces into the table
	} else {
	    double availableSpace = 7 - tempCol;
	    double spaceToFill = endDate - startDate + 1;
	    
	    while (availableSpace < spaceToFill) {
		//create panel of available space
		double cellSize = availableSpace*COL_OFFSET;
		
		StackPane gridCell = setUpGrid(t, indexToFit, tempRow, tempCol,
			cellSize);
		
		//put the panel in
		monthContainer.getChildren().add(gridCell);
		
		//fill virtual mem
		for (int i = tempCol; i < 7; i++) {
		    calendarMonth[tempRow][i][indexToFit] = 1;
		}
		
		//subtract remaining space
		availableSpace = 7;
		spaceToFill = spaceToFill - cellSize/COL_OFFSET;
		tempCol = 0;
		tempRow += 1;
	    }
	    
	    double cellSize = (spaceToFill)*COL_OFFSET;
	    StackPane gridCell;
	    
	    gridCell = setUpGrid(t, isDeadline, indexToFit, tempRow, tempCol,
		    cellSize);
		
	    //put the panel in
	    monthContainer.getChildren().add(gridCell);
	    
	  //fill virtual mem
	  while(cellSize > 0) {
	      calendarMonth[tempRow][tempCol][indexToFit] = 1;
	      cellSize -= COL_OFFSET;
	      tempCol += 1;
	  }
	}
    }


    private static StackPane setUpGrid(Task t, boolean isDeadline,
	    int indexToFit, int tempRow, int tempCol, double cellSize) {
	StackPane gridCell;
	if (isDeadline) {
	gridCell = createGridCell(cellSize, 15, "deadlistpanel", Pos.CENTER_LEFT); 
	} else {
	gridCell = createGridCell(cellSize, 15, "listpanel", Pos.CENTER_LEFT);
	}
	
	setContent(DESCRIPTION_OFFSET + t.getDescription(), gridCell, false);
	gridCell.setLayoutX(tempCol*COL_OFFSET);
	gridCell.setLayoutY(tempRow*ROW_OFFSET + HEADING_OFFSET + DATE_OFFSET + indexToFit*INDEX_OFFSET);
	return gridCell;
    }


    private static StackPane setUpGrid(Task t, int indexToFit, int tempRow,
	    int tempCol, double cellSize) {
	StackPane gridCell = createGridCell(cellSize, 15, "listpanel", Pos.CENTER_LEFT);
	
	setContent(DESCRIPTION_OFFSET + t.getDescription(), gridCell, false);
	gridCell.setLayoutX(tempCol*COL_OFFSET);
	gridCell.setLayoutY(calculateY(indexToFit, tempRow));
	return gridCell;
    }


    private static double calculateY(int indexToFit, int tempRow) {
	return tempRow*ROW_OFFSET + HEADING_OFFSET + DATE_OFFSET + indexToFit*INDEX_OFFSET;
    }


    private static void addToOverFlowList(boolean isDeadline, int tempRow,
	    int tempCol) {
	if (isDeadline) {
	    deadlineOverFlow[tempRow][tempCol] = deadlineOverFlow[tempRow][tempCol] + 1;
	} else {
	    eventOverFlow[tempRow][tempCol] = eventOverFlow[tempRow][tempCol] + 1;
	}
    }


    private static int lookForSpareIndex(int row, int col) {
	int indexToFit = 0;
	for (int i = 0; i < 4; i++) {
	    if (calendarMonth[row][col][i] == 0) {
		indexToFit = i;
		break;
	    }
	}
	return indexToFit;
    }


    private static void setDeadlineTaskToLastWholeDay(Calendar start,
	    Calendar end) {
	start.set(Calendar.HOUR_OF_DAY, 0);
	start.set(Calendar.MINUTE, 0);
	end.set(Calendar.HOUR_OF_DAY, 23);
	end.set(Calendar.MINUTE, 59);
	end.set(Calendar.DAY_OF_MONTH, start.get(Calendar.DAY_OF_MONTH));
	end.set(Calendar.MONTH, start.get(Calendar.MONTH));
	end.set(Calendar.YEAR, start.get(Calendar.YEAR));
    }
    
    private static void fillInAllOverFlow(){
	
	for (int row = 0; row < 6; row++) {
	    for (int col = 0; col < 7; col++) {
		String eventString = null;
		
		if (eventOverFlow[row][col] > 0) {
		    eventString = "+" + eventOverFlow[row][col] + "E";
		}
		
		String deadlineString = null;
		if (deadlineOverFlow[row][col] > 0) {
		    deadlineString ="+" + deadlineOverFlow[row][col] + "D";
		}
		
		if (eventString != null && deadlineString != null) {
		    StackPane gridCell1 = createGridCell(50, 15, "listpanel", Pos.CENTER);
		    setContent(eventString, gridCell1, false);
		    gridCell1.setLayoutX(col*COL_OFFSET);
		    gridCell1.setLayoutY(row*ROW_OFFSET + HEADING_OFFSET + DATE_OFFSET + 4*INDEX_OFFSET);
		    
		    StackPane gridCell2 = createGridCell(50, 15, "deadlistpanel", Pos.CENTER);
		    setContent(deadlineString, gridCell2, false);
		    gridCell2.setLayoutX(col*COL_OFFSET + COL_OFFSET/2);
		    gridCell2.setLayoutY(row*ROW_OFFSET + HEADING_OFFSET + DATE_OFFSET + 4*INDEX_OFFSET);
		    
		    monthContainer.getChildren().add(gridCell1);
		    monthContainer.getChildren().add(gridCell2);
		} else if (eventString != null) {
		    StackPane gridCell1 = createGridCell(100, 15, "listpanel", Pos.CENTER);
		    setContent(eventString, gridCell1, false);
		    gridCell1.setLayoutX(col*COL_OFFSET);
		    gridCell1.setLayoutY(row*ROW_OFFSET + HEADING_OFFSET + DATE_OFFSET + 4*INDEX_OFFSET);
		    monthContainer.getChildren().add(gridCell1);
		    
		} else if (deadlineString != null) {
		    StackPane gridCell2 = createGridCell(100, 15, "deadlistpanel", Pos.CENTER);
		    setContent(deadlineString, gridCell2, false);
		    gridCell2.setLayoutX(col*COL_OFFSET);
		    gridCell2.setLayoutY(row*ROW_OFFSET + HEADING_OFFSET + DATE_OFFSET + 4*INDEX_OFFSET);
		    monthContainer.getChildren().add(gridCell2);
		}
	    }
	}
    }
    
    private static int numberofEntries(int[] is) {
	int sum = 0;
	for (int i : is) {
	    sum += i;
	}
	
	return sum;
    }
    
    private static ArrayList<Task> keepTasksInThisMonth(ArrayList<Task> eventList) {
	ArrayList<Task> events = new ArrayList<Task>();
	if (eventList != null) {
	    for (int i = 0; i < eventList.size(); i++) {
		Task t = eventList.get(i);
		if (fallsWithin(t)) {
		    events.add(t);
		}
	    }
	}
	
	return events;
    }
	
    private static boolean fallsWithin(Task t) {
	boolean isFallsWithin = false;
	
	Calendar start = dateToCal(t.getTimeSpan().getStart());
	Calendar end = dateToCal(t.getTimeSpan().getEnd());
	
	Calendar startOfThisMonth = getStartOfThisMonth();
	Calendar endOfPreviousMonth = getEndOfPreviousMonth();
	Calendar endOfThisMonth = getEndOfThisMonth();
	Calendar startOfNextMonth = getStartOfNextMonth();
	
	Date newStart;
	Date newEnd;
	
	if (end.get(Calendar.YEAR) == 1950) {
	    if (start.after(endOfPreviousMonth) && start.before(startOfNextMonth)) {
		isFallsWithin = true;
	    }
	} else {
	    if (start.before(startOfThisMonth)) {
		newStart = startOfThisMonth.getTime();
		t.getTimeSpan().setStart(newStart);
		if (end.before(endOfPreviousMonth)) {
		    isFallsWithin = false;
		} else if (end.after(endOfThisMonth)) {
		    isFallsWithin = true;
		    newEnd = endOfThisMonth.getTime();
		    t.getTimeSpan().setEnd(newEnd);
		} else {
		    isFallsWithin = true;
		}
	    } else {
		if (start.after(endOfThisMonth)) {
		    isFallsWithin = false;
		} else if (end.after(endOfThisMonth)) {
		    isFallsWithin = true;
		    newEnd = endOfThisMonth.getTime();
		    t.getTimeSpan().setEnd(newEnd);
		} else {
		    isFallsWithin = true;
		}
	    }
	}
	
	
	return isFallsWithin;
    }
    
    private static Calendar getEndOfPreviousMonth() {
	Calendar cal = Calendar.getInstance();
	int year = thisMonth.get(Calendar.YEAR);
	int month = thisMonth.get(Calendar.MONTH);
	
	cal.set(Calendar.DAY_OF_MONTH, 1);
	if (month == 0) {
	    cal.set(Calendar.YEAR, year - 1);
	    cal.set(Calendar.MONTH, 11);
	} else {
	    cal.set(Calendar.YEAR, year);
	    cal.set(Calendar.MONTH, month - 1);
	}
	
	int daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
	cal.set(Calendar.DAY_OF_MONTH, daysInMonth);
	cal.set(Calendar.HOUR_OF_DAY, 23);
	cal.set(Calendar.MINUTE, 59);
	cal.set(Calendar.SECOND, 59);
	
	return cal;
    }
    
    private static Calendar getStartOfThisMonth() {
	Calendar cal = Calendar.getInstance();
	
	int year = thisMonth.get(Calendar.YEAR);
	int month = thisMonth.get(Calendar.MONTH);
	
	cal.set(Calendar.YEAR, year);
	cal.set(Calendar.MONTH, month);
	cal.set(Calendar.DAY_OF_MONTH, 1);
	cal.set(Calendar.HOUR_OF_DAY, 0);
	cal.set(Calendar.MINUTE, 0);
	cal.set(Calendar.SECOND, 0);
	
	return cal;
    }
    
    private static Calendar getEndOfThisMonth() {
	Calendar cal = Calendar.getInstance();
	int year = thisMonth.get(Calendar.YEAR);
	int month = thisMonth.get(Calendar.MONTH);
	int daysInMonth = thisMonth.getActualMaximum(Calendar.DAY_OF_MONTH);
	
	cal.set(Calendar.YEAR, year);
	cal.set(Calendar.DAY_OF_MONTH, 1);
	cal.set(Calendar.MONTH, month);
	cal.set(Calendar.DAY_OF_MONTH, daysInMonth);
	cal.set(Calendar.HOUR_OF_DAY, 23);
	cal.set(Calendar.MINUTE, 59);
	cal.set(Calendar.SECOND, 59);
	
	return cal;
    }
    
    private static Calendar getStartOfNextMonth() {
	Calendar cal = Calendar.getInstance();
	int year = thisMonth.get(Calendar.YEAR);
	int month = thisMonth.get(Calendar.MONTH);
	
	cal.set(Calendar.DAY_OF_MONTH, 1);
	if (month == 11) {
	    cal.set(Calendar.YEAR, year + 1);
	    cal.set(Calendar.MONTH, 0);
	} else {
	    cal.set(Calendar.YEAR, year);
	    cal.set(Calendar.MONTH, month + 1);
	}
	
	cal.set(Calendar.HOUR_OF_DAY, 0);
	cal.set(Calendar.MINUTE, 0);
	cal.set(Calendar.SECOND, 0);
	
	return cal;
    }


    private static double calculateTimeDifference(Task t) {
	TimeSpan span = t.getTimeSpan();
	Calendar start = dateToCal(span.getStart());
	Calendar end = dateToCal(span.getEnd());
	
	double differenceInMinutes = calculateTime(start, end);
	return differenceInMinutes;
    }


    private static double calculateTime(Calendar start, Calendar end) {
	int startDay = start.get(Calendar.DAY_OF_MONTH);
	int endDay = end.get(Calendar.DAY_OF_MONTH);
	
	int startHour = start.get(Calendar.HOUR_OF_DAY);
	int endHour = end.get(Calendar.HOUR_OF_DAY);
	
	int startMinute = start.get(Calendar.MINUTE);
	int endMinute = end.get(Calendar.MINUTE);
	
	double differenceInMinutes = (endDay - startDay)*1440 + 
		(endHour - startHour)*60 +
		(endMinute - startMinute);
	return differenceInMinutes;
    }
    
    private static Calendar dateToCal(Date d) {
	
	if (d == null) {
	    return null;
	}
	
	Calendar cal = Calendar.getInstance();
	cal.setTime(d);
	
	return cal;
    }
    
    private static void populateLayOut() throws Exception {
	populateMonthAndYear();
	populateDays();
	populateDates();
	resizeLayOut();
    }


    private static void resizeLayOut() {
	if (resize) {
	    monthContainer.setMinHeight(572.999977929685);
	    monthContainer.setMaxHeight(572.999977929685);
	    monthContainer.setPrefHeight(572.999977929685);
	}
    }


    private static void populateDates() {
	thisMonth.set(Calendar.DAY_OF_MONTH, 1);
	int day = thisMonth.get(Calendar.DAY_OF_WEEK);
	int daysInMonth = thisMonth.getActualMaximum(Calendar.DAY_OF_MONTH);
	int row = 1;
	int col = day - 1;
	for (int i = 1; i <= daysInMonth; i++) {
	    if (col > 6) {
		col = col%7;
		row++;
	    }
	    
	    if (row < 6) {
		resize = true;
	    } else {
		resize = false;
	    }
	    
	    Pane gridCell = new Pane();
	    gridCell.prefHeight(100);
	    gridCell.prefWidth(100);
	    Label dateLabel = new Label();
	    dateLabel.setText("  " + i);
	    dateLabel.setFont(Font.font(null, FontWeight.BOLD, 12));
	    gridCell.getChildren().add(dateLabel);
	    monthTable.add(gridCell, col, row, 1, 1);
	    col++;
	}
    }


    private static void populateDays() {
	for (int i = 0; i < 7; i++) {
	    StackPane gridCell = createGridCell(100, 25, "dayHeading", Pos.CENTER);
	    setContent(days[i], gridCell, true);
	    monthTable.add(gridCell, i, 0, 1, 1);
	}
    }


    private static void populateMonthAndYear() throws Exception {
	int year = thisMonth.get(Calendar.YEAR);
	int month = thisMonth.get(Calendar.MONTH) + 1;
	String monthInString = intToMonth(month);
	
	if (monthInString == null) {
	    throw new Exception("Date given is invalid. " + thisMonth.toString());
	}
	
	String content = monthInString + " " + year;
	monthLabel.setText(content);
    }


    private static void setContent(String content,
	StackPane gridCell, boolean isBold) {
	Label yearLabel = new Label();
	yearLabel.setText(content);
	yearLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
	
	if (isBold) {
	    yearLabel.setFont(Font.font(null, FontWeight.BOLD, 12));
	}
	
	gridCell.getChildren().add(yearLabel);
    }


    private static StackPane createGridCell(double width, double height, String color, Pos pos) {
	StackPane gridCell = new StackPane();
	gridCell.setPrefWidth(width);
	gridCell.setPrefHeight(height);
	gridCell.setAlignment(pos);
	gridCell.getStyleClass().add(color);
	return gridCell;
    }
    
    
    private static String intToMonth(int month) {
	String monthString;
        switch (month) {
            case 1:  monthString = "January";
                     break;
            case 2:  monthString = "February";
                     break;
            case 3:  monthString = "March";
                     break;
            case 4:  monthString = "April";
                     break;
            case 5:  monthString = "May";
                     break;
            case 6:  monthString = "June";
                     break;
            case 7:  monthString = "July";
                     break;
            case 8:  monthString = "August";
                     break;
            case 9:  monthString = "September";
                     break;
            case 10: monthString = "October";
                     break;
            case 11: monthString = "November";
                     break;
            case 12: monthString = "December";
                     break;
            default: monthString = null;
                     break;
        }
        
        return monthString;
    }
    
    private static ArrayList<Task> duplicateToArrayList(PriorityQueue<Task> pq) {
	ArrayList<Task> dup = new ArrayList<Task>();
	
	if (pq == null) {
	    return new ArrayList<Task>();
	}
	
	for (Task t : pq) {
	    String description = t.getDescription();
	    TimeSpan timeSpan = t.getTimeSpan();
	    boolean urgent = t.getUrgent();
	    Reminder reminder = t.getReminder();
	    
	    Task clone = new Task(description, timeSpan, urgent, reminder);
	    dup.add(clone);
	}
	
	Collections.sort(dup, comparator);
	
	return dup;
    }
}
